package com.kalle.workflow.service.impl;

import com.kalle.workflow.elements.Deployment;
import com.kalle.workflow.elements.SequenceFlow;
import com.kalle.workflow.elements.Task;
import com.kalle.workflow.enums.TaskType;
import com.kalle.workflow.mapper.DeploymentMapper;
import com.kalle.workflow.mapper.SequenceFlowMapper;
import com.kalle.workflow.mapper.TaskMapper;
import com.kalle.workflow.query.DeploymentQuery;
import com.kalle.workflow.service.DeploymentService;
import com.kalle.workflow.util.IdGenerator;
import com.kalle.workflow.util.UUIDGenerator;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class DeploymentServiceImpl implements DeploymentService {

    private final SqlSession sqlSession;
    private final IdGenerator idGenerator;

    private final DeploymentMapper deploymentMapper;
    private final TaskMapper taskMapper;
    private final SequenceFlowMapper sequenceFlowMapper;

    public DeploymentServiceImpl(SqlSession sqlSession, IdGenerator idGenerator) {
        this.sqlSession = sqlSession;
        this.deploymentMapper = sqlSession.getMapper(DeploymentMapper.class);
        this.taskMapper = sqlSession.getMapper(TaskMapper.class);
        this.sequenceFlowMapper = sqlSession.getMapper(SequenceFlowMapper.class);
        this.idGenerator = Optional.ofNullable(idGenerator).orElse(UUIDGenerator.INSTANCE);
    }

    /**
     * Deploy workflow resource by resource path.
     */
    @SuppressWarnings({ "unchecked" })
    public Deployment deployFromClassPath(String resourcePath) {
        try {
            SAXReader reader = new SAXReader();
            Document document = reader.read(Resources.getResourceAsFile(resourcePath));
            Element processElement = document.getRootElement();
            //save or update deployment
            Deployment deployment = saveOrUpdateDeployment(processElement);
            //task map
            Map<String, String> taskMap = new HashMap<>();
            //save start event
            saveStartEvents(processElement.elements("startEvent"), deployment, taskMap);
            //save end event
            saveEndEvents(processElement.elements("endEvent"), deployment, taskMap);
            //save user task
            saveUserTasks(processElement.elements("userTask"), deployment, taskMap);
            //save exclusive gateway
            saveExclusiveGateways(processElement.elements("exclusiveGateway"), deployment, taskMap);
            //save sequence flow
            saveSequenceFlows(processElement.elements("sequenceFlow"), deployment, taskMap);
            //session commit
            sqlSession.commit();
            return deployment;
        } catch (Throwable e) {
            sqlSession.rollback();
            throw new RuntimeException(e);
        }
    }

    /**
     * Save or update deployment and other components.
     */
    private Deployment saveOrUpdateDeployment(Element processElement) {
        String name = processElement.attributeValue("name");
        String processKey = processElement.attributeValue("key");
        Deployment deploymentInDB = deploymentMapper.selectByProcessKey(processKey);
        if (null != deploymentInDB) {
            deploymentMapper.deleteByProcessKey(processKey);
            sequenceFlowMapper.deleteByDeploymentId(deploymentInDB.getId());
        }
        Deployment deployment = new Deployment();
        deployment.setId(idGenerator.nextId());
        deployment.setName(name);
        deployment.setProcessKey(processKey);
        deploymentMapper.insert(deployment);
        return deployment;
    }

    /**
     * Save start events.
     */
    private void saveStartEvents(List<Element> startEventElements, Deployment deployment, Map<String, String> taskMap) {
        for (Element startEventElement : startEventElements) {
            Task startEvent = new Task();
            startEvent.setId(idGenerator.nextId());
            startEvent.setTaskType(TaskType.START_EVENT);
            startEvent.setName(startEventElement.attributeValue("name"));
            startEvent.setLabel("startEvent");
            startEvent.setDeploymentId(deployment.getId());
            taskMapper.insert(startEvent);
            taskMap.put(startEvent.getName(), startEvent.getId());
        }
    }

    /**
     * Save end events.
     */
    private void saveEndEvents(List<Element> endEventElements, Deployment deployment, Map<String, String> taskMap) {
        for (Element endEventElement : endEventElements) {
            Task endEvent = new Task();
            endEvent.setId(idGenerator.nextId());
            endEvent.setTaskType(TaskType.END_EVENT);
            endEvent.setName(endEventElement.attributeValue("name"));
            endEvent.setLabel("endEvent");
            endEvent.setDeploymentId(deployment.getId());
            taskMapper.insert(endEvent);
            taskMap.put(endEvent.getName(), endEvent.getId());
        }
    }

    /**
     * Save user tasks.
     */
    private void saveUserTasks(List<Element> userTaskElements, Deployment deployment, Map<String, String> taskMap) {
        for (Element userTaskElement : userTaskElements) {
            Task userTask = new Task();
            userTask.setId(idGenerator.nextId());
            userTask.setTaskType(TaskType.USER_TASK);
            userTask.setName(userTaskElement.attributeValue("name"));
            userTask.setLabel(userTaskElement.attributeValue("label"));
            userTask.setDeploymentId(deployment.getId());
            taskMapper.insert(userTask);
            taskMap.put(userTask.getName(), userTask.getId());
        }
    }

    /**
     * Save exclusive gateways.
     */
    private void saveExclusiveGateways(List<Element> exclusiveGatewayElements, Deployment deployment, Map<String, String> taskMap) {
        for (Element exclusiveGatewayElement : exclusiveGatewayElements) {
            Task exclusiveGateway = new Task();
            exclusiveGateway.setId(idGenerator.nextId());
            exclusiveGateway.setTaskType(TaskType.EXCLUSIVE_GATEWAY);
            exclusiveGateway.setName(exclusiveGatewayElement.attributeValue("name"));
            exclusiveGateway.setLabel("exclusiveGateway");
            exclusiveGateway.setDeploymentId(deployment.getId());
            taskMapper.insert(exclusiveGateway);
            taskMap.put(exclusiveGateway.getName(), exclusiveGateway.getId());
        }
    }

    /**
     * Save sequence flows.
     */
    private void saveSequenceFlows(List<Element> sequenceFlowElements, Deployment deployment, Map<String, String> taskMap) {
        for (Element sequenceFlowElement : sequenceFlowElements) {
            SequenceFlow sequenceFlow = new SequenceFlow();
            sequenceFlow.setId(idGenerator.nextId());
            sequenceFlow.setName(sequenceFlowElement.attributeValue("name"));
            sequenceFlow.setDeploymentId(deployment.getId());
            sequenceFlow.setSourceRefId(taskMap.get(sequenceFlowElement.attributeValue("sourceRef")));
            sequenceFlow.setTargetRefId(taskMap.get(sequenceFlowElement.attributeValue("targetRef")));
            sequenceFlow.setExpression(sequenceFlowElement.attributeValue("expression"));
            sequenceFlowMapper.insert(sequenceFlow);
        }
    }

    /**
     * Provide chain call mode for query deployment.
     */
    public DeploymentQuery createQuery() {
        return new DeploymentQuery(deploymentMapper);
    }

}
